﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using Ingenious.Core;

namespace Ingenious.Graph
{
    public enum GraphLock
    {
        Read,
        Upgradable,
        Write,
    }

    /// <summary>
    /// Defines a read, upgradable or write transaction scope on a graph
    /// Changes to the graph are dispatched to the graph store when the
    /// graph scope closes (is disposed)
    /// </summary>
    /// <typeparam name="TTripleId">The ID type for storing the triple in the graph store</typeparam>
    public class GraphScope<TTripleId> : IDisposable
    {
        private readonly Graph<TTripleId> m_graph;
        private readonly GraphLock m_graphLock;
        private readonly FetchMode m_fetchMode;

        private volatile bool m_disposed = false;

        private readonly object m_tripleChangeLock = new object();

        private readonly ManualResetEvent m_waitForPendingChangesEvent = new ManualResetEvent(false);

        // For delay-loaded asynchronous properties that need loading before checking for changes
        private readonly HashSet<TTripleId> m_pendingTripleChangeIds = new HashSet<TTripleId>();
        private readonly Dictionary<TTripleId, TripleChange<TTripleId>> m_pendingTripleChangeLookup = new Dictionary<TTripleId, TripleChange<TTripleId>>();

        // Changes to be applied once the transaction is disposed
        private readonly HashSet<TripleChange<TTripleId>> m_tripleChanges = new HashSet<TripleChange<TTripleId>>();

        #region Object Lifetime
        public GraphScope(Graph<TTripleId> graph, GraphLock graphLock, FetchMode fetchMode = FetchMode.Synchronous)
        {
            if (graph == null)
            {
                throw new ArgumentNullException("graph");
            }

            m_graph = graph;
            m_graphLock = graphLock;
            m_fetchMode = fetchMode;

            graph.Proxy.StartTransaction(this, graph);

            switch (graphLock)
            {
            case GraphLock.Read:
                {
                    graph.Lock.EnterReadLock();
                }
                break;
            case GraphLock.Upgradable:
                {
                    graph.Lock.EnterUpgradeableReadLock();
                }
                break;
            case GraphLock.Write:
                {
                    graph.Lock.EnterWriteLock();
                }
                break;
            }
        }

        private void ThrowIfDisposed()
        {
            if (m_disposed)
            {
                throw new ObjectDisposedException("GraphScope");
            }
        }

        public void Dispose()
        {
            if (!m_disposed)
            {
                switch (m_graphLock)
                {
                case GraphLock.Read:
                    {
                        m_graph.Lock.ExitReadLock();
                    }
                    break;
                case GraphLock.Upgradable:
                    {
                        m_graph.Lock.ExitUpgradeableReadLock();
                    }
                    break;
                case GraphLock.Write:
                    {
                        m_graph.Lock.ExitWriteLock();
                    }
                    break;
                }

                if (FetchMode == Ingenious.Graph.FetchMode.Asynchronous)
                {
                    // If in asynchronous mode, wait until the pending triple change
                    // detection count drops to zero before allowing the transaction to close
                    m_waitForPendingChangesEvent.WaitOne();
                }

                // Compress the change list so only required changes are sent (for efficiency)
                ChangeEnumerationHelper.CompressChangeList<TTripleId>(m_tripleChanges);
                Graph.Proxy.EndTransaction(this,
                    m_tripleChanges.Where(tc => tc.TripleChangeType == TripleChangeType.Add).Select(tc => tc.Triple),
                    m_tripleChanges.Where(tc => tc.TripleChangeType == TripleChangeType.Modify).Select(tc => tc.Triple),
                    m_tripleChanges.Where(tc => tc.TripleChangeType == TripleChangeType.Remove).Select(tc => tc.Triple));

                m_tripleChanges.Clear();

                m_disposed = true;
            }
        }
        #endregion

        #region Change Enumeration

        /// <summary>
        /// Returns whether the triple with the given ID has any changes pending
        /// </summary>
        /// <param name="id">Triple ID to check for pending changes</param>
        /// <returns>Whether a change is currently pending</returns>
        public bool IsChangePending(TTripleId id)
        {
            ThrowIfDisposed();

            lock (m_tripleChangeLock)
            {
                return m_pendingTripleChangeIds.Contains(id);
            }
        }

        /// <summary>
        /// Obtains any pending change for the triple with specific triple ID
        /// </summary>
        /// <param name="id">The triple graph store ID</param>
        /// <param name="pendingTripleChange">The pending triple change</param>
        /// <returns>Whether pending change has been found</returns>
        public bool TryGetPendingChange(TTripleId id, out TripleChange<TTripleId> pendingTripleChange)
        {
            ThrowIfDisposed();

            pendingTripleChange = null;

            lock (m_tripleChangeLock)
            {
                return m_pendingTripleChangeLookup.TryGetValue(id, out pendingTripleChange);
            }
        }

        /// <summary>
        /// Registers that change detection is pending
        /// 
        /// This is required in asynchronous loading mode where we may not know the
        /// current value associated with the triple ID until it is pulled from the
        /// data store. Once it is loaded we know if its original value has changed.
        /// </summary>
        /// <param name="id">The ID of the triple</param>
        public void RegisterPendingChange(TripleChange<TTripleId> tripleChange)
        {
            if (m_fetchMode == Ingenious.Graph.FetchMode.Synchronous)
            {
                throw new InvalidOperationException("Registering pending changes can only happen in asynchronous mode");
            }

            lock (m_tripleChangeLock)
            {
                if (!m_pendingTripleChangeIds.Contains(tripleChange.Triple.ID))
                {
                    m_pendingTripleChangeIds.Add(tripleChange.Triple.ID);
                    m_pendingTripleChangeLookup.Add(tripleChange.Triple.ID, tripleChange);
                }
            }
        }

        // TODO: For efficiency, either compound changes into a result per-change,
        // or on ending the transaction

        // Per-change will be more memory efficient
        public void EnumerateChange(TripleChange<TTripleId> tripleChange)
        {
            ThrowIfDisposed();

            if (!m_disposed)
            {
                lock (m_tripleChangeLock)
                {
                    m_tripleChanges.Add(tripleChange);

                    if (m_fetchMode == Ingenious.Graph.FetchMode.Asynchronous)
                    {
                        // Cancel the pending change as it has been processed
                        if (m_pendingTripleChangeIds.Contains(tripleChange.Triple.ID))
                        {
                            m_pendingTripleChangeIds.Remove(tripleChange.Triple.ID);
                        }

                        if (m_pendingTripleChangeIds.Count != 0)
                        {
                            // There are pending change detections - set wait handle to non-signalled
                            if (m_waitForPendingChangesEvent.WaitOne(0, false))
                            {
                                m_waitForPendingChangesEvent.Reset();
                            }
                        }
                        else
                        {
                            if (!m_waitForPendingChangesEvent.WaitOne(0, false))
                            {
                                m_waitForPendingChangesEvent.Set();
                            }
                        }
                    }
                }
            }
        }
        #endregion

        #region Properties
        public GraphLock GraphLock
        {
            get { return m_graphLock; }
        }

        public FetchMode FetchMode
        {
            get { return m_fetchMode; }
        }

        public Graph<TTripleId> Graph
        {
            get { return m_graph; }
        }

        public bool Disposed
        {
            get { return m_disposed; }
        }
        #endregion
    }
}
